package main

import (
	"context"
	"fmt"
	"time"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"go.mongodb.org/mongo-driver/mongo/readconcern"
	"go.mongodb.org/mongo-driver/mongo/readpref"
)

/* driver针对mongo操作的事务 */

// mongo.NewSessionContext  /* 最不方便的 */
func TransactionSC(ctx context.Context) error {
	stuCollection := MongoClient.Database("mytest").Collection("study")

	// session  需要主动控制session
	session, err := MongoClient.StartSession()
	if nil != err {
		fmt.Println("Start the Session fail ,error :", err)
		return err
	}
	defer session.EndSession(ctx)

	// session context  主动控制sessionCtx
	sessionCtx := mongo.NewSessionContext(ctx, session)

	// start transaction  需要主动控制事务回滚与提交
	if err = session.StartTransaction(); err != nil {
		fmt.Println("start the transaction fail , error: ", err)
		return err
	}

	// insert one to collection
	insertRes, err := stuCollection.InsertOne(sessionCtx, bson.M{"name": "hahaha"})
	if nil != err {
		fmt.Println("insert to collection error：", err)
		// 使用新的ctx确保成功，即使原本的ctx已经超时
		_ = session.AbortTransaction(context.Background())
		return err
	}

	fmt.Println("insert the data success!")

	// 再一次例子
	var result bson.M
	if err = stuCollection.FindOne(sessionCtx, bson.D{{"_id", insertRes.InsertedID}}).
		Decode(&result); err != nil {
		fmt.Println("find the data fail , will AbortTransation")
		_ = session.AbortTransaction(context.Background())
		return err
	}

	fmt.Println("the data is ", result)

	// 事务执行成功则提交commit
	if err = session.CommitTransaction(context.Background()); err != nil {
		fmt.Println()
		panic(err)
	}
	return nil
}

/* mongo.NewSessionContext需要用户主动控制session与transaction，相当于自己掌握事务的所有组件 */

// mongo.WithSession模式
func TransactionWC(ctx context.Context) error {

	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
	defer cancel()

	collection := MongoClient.Database("mytest").Collection("study")

	// 构造session Optins
	sessionOps := options.Session().SetDefaultReadConcern(readconcern.Majority())
	session, err := MongoClient.StartSession(sessionOps)
	if nil != err {
		fmt.Println("start the session fail , error is :", err)
		return err
	}
	// 使用新的context就不受原本的影响
	defer session.EndSession(context.TODO())
	/* 通过mongo的options预置的策略管理sessionCtx，用户只需要主动管理session就可以。但是仍然需要主动控制事务。
	除非特定需要，否则sessionCtx都可以直接让mongo自己控制 */

	// session准备完毕就开始事务transaction

	sfun := func(sessionCtx mongo.SessionContext) error {
		if err = session.StartTransaction(); err != nil {
			fmt.Println("start the transaction error ", err)
			return err
		}

		insertRes, err := collection.InsertOne(ctx, bson.M{"name": "大boss"})
		if nil != err {
			fmt.Println("插入失败")
			_ = session.AbortTransaction(context.Background())
			return err
		}

		var result bson.M
		if err = collection.FindOne(ctx, bson.M{"_id": insertRes.InsertedID}).Decode(&result); err != nil {
			fmt.Println("查询失败")
			// 失败则打断回滚
			_ = session.AbortTransaction(context.Background())
		}

		fmt.Println("执行操作成功：", result)
		return session.CommitTransaction(context.Background())
	}

	err = mongo.WithSession(ctx, session, sfun) // 核心执行方法
	/* 这种传入函数A作为参数，一般就是另外的参数是内部函数B的入参，函数B返回的结果作为传入的函数A的入参 */

	// checkerr(err) 一般需要这样
	return err
}

// client.UseSessionWithOptions模式 ， 只聚焦事务处理
func UseSessionWithOpts(ctx context.Context) error {
	ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
	defer cancel()

	collection := MongoClient.Database("mytest").Collection("study")

	// session
	sessionOps := options.Session().SetDefaultReadConcern(readconcern.Majority())

	/* 不需要主动管理session与sessionCtx，只需要管理事务提交 */
	// 内部使用了默认的session对象，并且调用返回了默认的sessionCtx作为fn的参数
	err := MongoClient.UseSessionWithOptions(ctx, sessionOps, func(sessionCtx mongo.SessionContext) error {
		// 这里面是具体要执行的操作
		if err := sessionCtx.StartTransaction(); err != nil { // 通过上下文开启事务
			return err
		}

		// 插入记录
		insertOneResult, err := collection.InsertOne(sessionCtx, bson.M{"name": "hahaah"})
		if err != nil {
			_ = sessionCtx.AbortTransaction(context.Background())
			fmt.Println("记录插入失败，回滚")
			return err
		}

		var res bson.M
		if err = collection.FindOne(sessionCtx, bson.M{"_id": insertOneResult.InsertedID}).Decode(&res); err != nil {
			_ = sessionCtx.AbortTransaction(context.Background())
			fmt.Println("查询记录失败")
			return nil
		}

		fmt.Println("事务执行成功：", res)
		return sessionCtx.CommitTransaction(context.Background()) // 返回的时候要提交事务
	})

	// 所以用户只需要关注事务的操作而不用管session
	if err != nil { // 这里是查看事务里面的操作执行有没有错误
		fmt.Println("have an error:", err)
		return err
	}
	return nil
}

// session.WithTransaction模式
func SessionWithTrans(ctx context.Context) error {
	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	// 准备相关对象
	collection := MongoClient.Database("mytest").Collection("study")

	sessionOps := options.Session().SetDefaultReadConcern(readconcern.Majority())
	session, err := MongoClient.StartSession(sessionOps) // 这里获取sessionimp对象
	if nil != err {
		fmt.Println("开启事务失败")
		return err
	}
	defer session.EndSession(context.Background())
	// 通过options获取session情况——只是获取session并不需要管sessionCtx

	// 设置 trancasion 读取优先级，从optins中读取相关配置
	transactionOps := options.Transaction().SetReadPreference(readpref.Primary())

	// 在执行mongo命令的时候再开启事务（传的应该还是func）
	/* trancasionOps作为内部调用开启事务的参数
	WithTransaction中会控制超时，执行之后会自动提交，检查出错也会abort
	*/
	res, err := session.WithTransaction(ctx, func(sessionCtx mongo.SessionContext) (interface{}, error) {
		insertOneRes, err := collection.InsertOne(sessionCtx, bson.M{"name": "简单", "level": 5})
		if nil != err {
			fmt.Println("插入记录失败")
			return nil, err
		}
		fmt.Println("记录插入成功！：", insertOneRes.InsertedID)

		// 执行查找——执行失败也会回滚事务
		var result struct {
			Name  string `bson:"name"`
			Level int    `bson:"level"`
		}
		singleResult := collection.FindOne(sessionCtx, bson.M{"_id": insertOneRes.InsertedID})
		if err = singleResult.Decode(&result); err != nil {
			return nil, err
		}
		return result, err
	},
		transactionOps, // 传入事务Options相关对象
	)
	/* 不需要自己启动提交事务 */
	if nil != err {
		fmt.Println("事务执行失败：", err)
		return err
	}
	fmt.Println("事务执行成功！", res)

	return nil
}
